package indigo.mybatis.dao;

import static java.util.Objects.requireNonNull;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;

import org.apache.ibatis.session.SqlSession;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;

import indigo.mybatis.dao.criterion.Criterion;
import indigo.mybatis.entity.EntityMetadata;
import indigo.mybatis.pagination.PagedRowBounds;
import indigo.util.PagedList;

public class SqlDao<T> {
	private static final String SELECT_ID = "select";
	private static final String SELECT_COUNT_ID = "selectCount";
	private static final String SELECT_EXIST_ID = "selectExist";
	private static final String SELECT_PROJECTION_ID = "selectProjection";
	private static final String INSERT_ID = "insert";
	private static final String UPDATE_ID = "update";
	private static final String DELETE_ID = "delete";

	private static final String METADATA_KEY = "metadata";
	private static final String CRITERION_KEY = "criterion";
	private static final String ORDERS_KEY = "orders";
	private static final String PARAMETERS_KEY = "parameters";
	private static final String PROJECTION_KEY = "projection";
	private static final String UPDATES_KEY = "updates";
	private static final String ENTITY_KEY = "entity";

	private final SqlSession sqlSession;
	private final EntityMetadata<T> metadata;
	private final Criterion criterion;
	private final List<Order> orders;
	private final Map<String, Object> parameters;

	public SqlDao(SqlSession sqlSession, EntityMetadata<T> metadata) {
		this.sqlSession = requireNonNull(sqlSession, "sqlSession cannot be null");
		this.metadata = requireNonNull(metadata, "metadata cannot be null");
		this.criterion = Criterion.nothing();
		this.orders = ImmutableList.of();
		this.parameters = ImmutableMap.of();
	}

	protected SqlDao(SqlDao<T> previous, Criterion extraCriterion, Iterable<? extends Order> extraOrders,
			Map<String, ?> extraParameters) {
		this.sqlSession = previous.getSqlSession();
		this.metadata = previous.getMetadata();

		this.criterion = Optional.ofNullable(extraCriterion)
				.map(criterion -> Criterion.and(previous.getCriterion(), criterion))
				.orElseGet(() -> Criterion.and(previous.getCriterion()));

		this.orders = Optional.ofNullable(extraOrders)
				.map(orders -> ImmutableList.copyOf(Iterables.concat(previous.getOrders(), extraOrders)))
				.orElseGet(() -> ImmutableList.copyOf(previous.getOrders()));

		this.parameters = Optional.ofNullable(extraParameters)
				.map(parameters -> {
					Map<String, Object> merged = Maps.newHashMap();
					merged.putAll(previous.getParameters());
					merged.putAll(extraParameters);
					return ImmutableMap.copyOf(merged);
				})
				.orElseGet(() -> ImmutableMap.copyOf(previous.getParameters()));
	}

	protected String getQualifiedId(String statementId) {
		return metadata.getType().getName() + "." + statementId;
	}

	public SqlSession getSqlSession() {
		return sqlSession;
	}

	public EntityMetadata<T> getMetadata() {
		return metadata;
	}

	public Criterion getCriterion() {
		return criterion;
	}

	public List<Order> getOrders() {
		return orders;
	}

	public Map<String, Object> getParameters() {
		return parameters;
	}

	public SqlDao<T> withCriterion(Criterion criterion) {
		return new SqlDao<T>(this, criterion, null, null);
	}

	public SqlDao<T> withOrders(Order... orders) {
		return withOrders(Arrays.asList(orders));
	}

	public SqlDao<T> withOrders(Collection<? extends Order> orders) {
		return new SqlDao<T>(this, null, orders, null);
	}

	public SqlDao<T> withParameter(String name, Object value) {
		return new SqlDao<T>(this, null, null, ImmutableMap.of(name, value));
	}

	public SqlDao<T> withParameters(Map<String, ?> parameters) {
		return new SqlDao<T>(this, null, null, parameters);
	}

	private Map<String, Object> newParameterMap(Consumer<Map<String, Object>> putter) {
		Map<String, Object> parameter = Maps.newHashMap();
		putter.accept(parameter);
		return parameter;
	}

	public T one() {
		return sqlSession.selectOne(getQualifiedId(SELECT_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
		}));
	}

	public <R> R one(String statementId) {
		return sqlSession.selectOne(getQualifiedId(statementId), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(PARAMETERS_KEY, parameters);
		}));
	}

	public <R> R one(Projection projection) {
		return sqlSession.selectOne(getQualifiedId(SELECT_PROJECTION_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(PROJECTION_KEY, projection);
		}));
	}

	public boolean exist() {
		return sqlSession.selectOne(getQualifiedId(SELECT_EXIST_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
		}));
	}

	public boolean exist(String statementId) {
		return sqlSession.selectOne(getQualifiedId(statementId), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(PARAMETERS_KEY, parameters);
		}));
	}

	public Number count() {
		return sqlSession.selectOne(getQualifiedId(SELECT_COUNT_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
		}));
	}

	public Number count(String statementId) {
		return sqlSession.selectOne(getQualifiedId(statementId), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(PARAMETERS_KEY, parameters);
		}));
	}

	public List<T> list() {
		return sqlSession.selectList(getQualifiedId(SELECT_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(ORDERS_KEY, orders);
		}));
	}

	public <R> List<R> list(String statementId) {
		return sqlSession.selectList(getQualifiedId(statementId), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(ORDERS_KEY, orders);
			p.put(PARAMETERS_KEY, parameters);
		}));
	}

	public <R> List<R> list(Projection projection) {
		return sqlSession.selectList(getQualifiedId(SELECT_PROJECTION_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(ORDERS_KEY, orders);
			p.put(PROJECTION_KEY, projection);
		}));
	}

	@SuppressWarnings("unchecked")
	public PagedList<T> pagedList(PagedRowBounds rowBounds) {
		return (PagedList<T>) sqlSession.selectList(getQualifiedId(SELECT_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(ORDERS_KEY, orders);
		}), rowBounds);
	}

	@SuppressWarnings("unchecked")
	public <R> PagedList<R> pagedList(String statementId, PagedRowBounds rowBounds) {
		return (PagedList<R>) sqlSession.selectList(getQualifiedId(statementId), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(ORDERS_KEY, orders);
			p.put(PARAMETERS_KEY, parameters);
		}), rowBounds);
	}

	@SuppressWarnings("unchecked")
	public <R> PagedList<R> pagedList(Projection projection, PagedRowBounds rowBounds) {
		return (PagedList<R>) sqlSession.selectList(getQualifiedId(SELECT_PROJECTION_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(ORDERS_KEY, orders);
			p.put(PROJECTION_KEY, projection);
		}), rowBounds);
	}

	public int insert(T entity) {
		return sqlSession.insert(getQualifiedId(INSERT_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(ENTITY_KEY, entity);
		}));
	}

	public int delete() {
		return sqlSession.delete(getQualifiedId(DELETE_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
		}));
	}

	public int delete(String statementId) {
		return sqlSession.delete(getQualifiedId(DELETE_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(PARAMETERS_KEY, parameters);
		}));
	}

	public int update(Collection<? extends Update> updates) {
		return sqlSession.update(getQualifiedId(UPDATE_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(UPDATES_KEY, updates);
		}));
	}

	public int update(Update... updates) {
		return update(Arrays.asList(updates));
	}

	public int update(String statementId, Collection<? extends Update> updates) {
		return sqlSession.delete(getQualifiedId(UPDATE_ID), newParameterMap(p -> {
			p.put(METADATA_KEY, metadata);
			p.put(CRITERION_KEY, criterion);
			p.put(PARAMETERS_KEY, parameters);
			p.put(UPDATES_KEY, updates);
		}));
	}

	public int update(String statementId, Update... updates) {
		return update(statementId, Arrays.asList(updates));
	}
}
